"""
Occlusion strategy interface.

Allows swapping different definitions of "occlusion":
- Representative point (LoS) to a single point
- Cylinder sampling (LoS to many points)
- Exact geometry library (OCC/VTK), etc.
"""

from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol, Optional, Callable
import numpy as np
from ...utils.math3d import distance_point_to_segment, segment_intersects_sphere
from ...type.models import Missile, CloudModel, DropPlan, CylinderTarget, Vec3


class OcclusionStrategy(Protocol):
    """
    Strategy interface: "Is the missile's line-of-sight to the target occluded by the cloud at time t?"
    """
    def is_occluded(self, t: float, missile: Missile, target: CylinderTarget,
                    plan: DropPlan, model: CloudModel) -> bool: ...


@dataclass
class PointLoSStrategy:
    """
    LoS occlusion using a single representative point on the cylinder, e.g. the geometric center.

    Definition:
      At time t, if distance from the cloud center to the segment [M(t), T_point] <= r, and the cloud
      is effective (t in [t_det, t_det+life]), we say "occluded".
    """
    which_point: str = "center"  # "center" | "top_center" | "bottom_center"

    def _select_point(self, target: CylinderTarget) -> Vec3:
        if self.which_point == "top_center":
            return target.top_center
        elif self.which_point == "bottom_center":
            return target.bottom_center
        return target.center_point

    def is_occluded(self, t: float, missile: Missile, target: CylinderTarget,
                    plan: DropPlan, model: CloudModel) -> bool:
        C = plan.center_func(model)(t)
        if C is None:
            return False
        M = missile.pos(t)
        Tpt = self._select_point(target)
        return distance_point_to_segment(M, Tpt, C) <= model.r + 1e-12


@dataclass
class CylinderSamplingStrategy:
    """
    Discretize the cylinder surface (by angle and height) and test LoS to each sample point.
    If the fraction of blocked points >= threshold, we mark the cylinder as occluded.

    Parameters
    ----------
    n_circ : int
        Number of samples along circumference per height layer.
    n_h : int
        Number of evenly spaced height layers (including top and bottom).
    threshold : float
        Fraction in [0,1]. 1.0 means "all sampled points blocked"; 0.5 means "majority blocked".
    """
    n_circ: int = 36
    n_h: int = 5
    threshold: float = 1.0

    def _sample_points(self, target: CylinderTarget) -> list[Vec3]:
        bx, by, bz = target.base_center
        zs = np.linspace(bz, bz + target.height, self.n_h)
        thetas = np.linspace(0.0, 2.0 * np.pi, self.n_circ, endpoint=False)
        pts = []
        for z in zs:
            for th in thetas:
                x = bx + target.radius * np.cos(th)
                y = by + target.radius * np.sin(th)
                pts.append(np.array([x, y, z], dtype=float))
        return pts

    def is_occluded(self, t: float, missile: Missile, target: CylinderTarget,
                    plan: DropPlan, model: CloudModel) -> bool:
        C = plan.center_func(model)(t)
        if C is None:
            return False
        M = missile.pos(t)
        pts = self._sample_points(target)
        blocked = 0
        for P in pts:
            if segment_intersects_sphere(M, P, C, model.r):
                blocked += 1
        frac = blocked / float(len(pts))
        return frac >= self.threshold


@dataclass
class ExactGeometryStrategy:
    """
    Placeholder for an exact (CAD) occlusion test using pythonocc-core / VTK.

    Implementations may, for example, construct B-Rep shapes:
      - a TopoDS_Edge for the segment [M(t), P] (or use the cylinder as a solid and intersect
        the ray [M(t), ∞) with the cylinder, then test sphere ∩ segment)
      - a TopoDS_Solid for the sphere (cloud) and for the finite cylinder (target)
      - compute intersection common(sphere, edge) to determine hit / occlusion.
    """
    backend: str = "pythonocc-core"

    def is_occluded(self, t: float, missile: Missile, target: CylinderTarget,
                    plan: DropPlan, model: CloudModel) -> bool:
        raise NotImplementedError(
            "ExactGeometryStrategy requires an external backend. "
            "See docs in this class for how to implement with pythonocc-core."
        )